package org.vaadin.nf4v.ext.config.beforenav;

import com.vaadin.ui.Component;
import org.vaadin.nf4v.core.NavigableApplication;
import org.vaadin.nf4v.core.NavigationException;
import org.vaadin.nf4v.core.config.PageConfig;
import org.vaadin.nf4v.core.navigation.NavigationManager;
import org.vaadin.nf4v.core.navigation.NavigationRequest;
import org.vaadin.nf4v.core.navigation.event.NavigationState;
import org.vaadin.nf4v.core.navigation.target.NavigationTarget;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * Utility class.
 * Provides access to before navigation listener method in page.
 */
public class ExtBeforeNavUtil {
    /**
     * @param config page config
     * @return page method marked with @BeforeNavigation annotation or null if no annotated method found
     */
    public static Method getBeforeNavigationListenerMethod(PageConfig config) {
        return config.getAttribute(BeforeNavigationAnnotationParser.BEFORE_NAVIGATION_METHOD_KEY);
    }

    /**
     * @param config page config
     * @return type of parameters of method annotated with @BeforeNavigation
     */
    public static MethodType getBeforeNavigationListenerMethodParamType(PageConfig config) {
        return config.getAttribute(BeforeNavigationAnnotationParser.BEFORE_NAVIGATION_METHOD_PARAMS_KEY);
    }

    /**
     * @param config page config
     * @return array of navigation states which method should listen or null if method subscribed for all states
     */
    public static NavigationState[] getBeforeNavigationListenerMethodSubscribedStates(PageConfig config) {
        return config.getAttribute(BeforeNavigationAnnotationParser.BEFORE_NAVIGATION_METHOD_EVENTS);

    }

    /**
     * @param config page config
     * @return true if page class has method annotated with @BeforeNavigation, false otherwise
     */
    public static boolean hasBeforeNavigationListenerMethod(PageConfig config) {
        return getBeforeNavigationListenerMethod(config) != null;
    }

    /**
     * @return array of navigation states which method should listen or null if method subscribed for all states or
     *         context is empty
     * @throws NavigationException if configuration for page not found
     */
    public static NavigationState[] getBeforeNavigationListenerMethodSubscribedStates()
            throws NavigationException {
        // context is empty
        if (NavigationManager.getNavigator().getCurrentPageTarget()==null) {
            return null;
        }

        Class<? extends Component> pageClass = NavigationManager.getNavigator().getCurrentPageTarget().getPageClass();
        String context = NavigationManager.getNavigationContext();
        PageConfig config = NavigableApplication.getCurrent().getNavigationConfig()
                .getPageConfig(context, pageClass);
        if (config == null) {
            throw new NavigationException("Page " + pageClass.getSimpleName() + " is not registered in context '" +
                    context + "'");
        }
        return getBeforeNavigationListenerMethodSubscribedStates(config);
    }

    /**
     * Method has extended logic: it will check all possible situations (allowed states range, method existence)
     *
     * @param request navigation request
     * @return true if current page is subscribed to current request state, false otherwise
     */
    public static boolean isNavigationStateSubscribed(NavigationRequest request) {
        NavigationState[] states = getBeforeNavigationListenerMethodSubscribedStates();
        if (request.getState().ordinal() < NavigationState.PAGE_NAVIGATION.ordinal()
                || request.getState().ordinal() > NavigationState.BEFORE_NAVIGATION.ordinal()) {
            // unreachable events for before navigation listeners 
            return false;
        }

        if (!hasBeforeNavigationListenerMethod()) {
            //no listener
            return false;
        }

        // subscription for all events
        if (states == null) return true;

        for (NavigationState state : states) {
            if (state == request.getState()) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return true if current page class has method annotated with @BeforeNavigation, false otherwise
     * @throws NavigationException if configuration for page not found
     */
    public static boolean hasBeforeNavigationListenerMethod()
            throws NavigationException {
        NavigationTarget currentTarget = NavigationManager.getNavigator().getCurrentPageTarget();
        if (currentTarget==null){
            return false;
        }
        Class<? extends Component> pageClass = currentTarget.getPageClass();
        String context = NavigationManager.getNavigationContext();
        PageConfig config = NavigableApplication.getCurrent().getNavigationConfig()
                .getPageConfig(context, pageClass);
        if (config == null) {
            throw new NavigationException("Page " + pageClass.getSimpleName() + " is not registered in context '" +
                    context + "'");
        }
        return hasBeforeNavigationListenerMethod(config);
    }

    /**
     * Calls before navigation listener method on page instance.
     * Provides NavigationRequest as parameter if required.
     *
     * @param request current navigation request
     * @throws NavigationException if configuration for page not found or page has no annotated method
     */
    public static void notifyBeforeNavigation(NavigationRequest request) throws NavigationException {
        NavigationTarget target = NavigationManager.getNavigator().getCurrentPageTarget();
        if (target == null) {
            // programmer error (wrong listener)
            throw new NavigationException("Can't invoke before navigation listener method cause context is empty.");
        }
        String context = NavigationManager.getNavigationContext();
        Class<? extends Component> pageClass = target.getPageClass();
        PageConfig config = NavigableApplication.getCurrent().getNavigationConfig()
                .getPageConfig(context, pageClass);
        if (config == null) {
            throw new NavigationException("Page " + pageClass.getSimpleName() + " is not registered in context '" +
                    context + "'");
        }
        if (!hasBeforeNavigationListenerMethod(config)) {
            throw new NavigationException("No method annotated with @" + BeforeNavigation.class.getSimpleName() +
                    " found for page " + pageClass.getSimpleName() + " in context '" + context + "'");
        }
        Method method = getBeforeNavigationListenerMethod(config);
        boolean recoverAccessibility = false;
        try {
            if (!method.isAccessible()) {
                method.setAccessible(true);
                recoverAccessibility = true;
            }

            switch (getBeforeNavigationListenerMethodParamType(config)) {
                case NO_PARAMS:
                    method.invoke(NavigationManager.getNavigator().getCurrentPage());
                    break;
                case REQUEST:
                    method.invoke(NavigationManager.getNavigator().getCurrentPage(), request);
                    break;
                default:
                    throw new RuntimeException("Configuration error: can't determine parameters type for method " +
                            config.getPageClass().getSimpleName() + "#" + method.getName() + " in context '" + context + "'");
            }

        } catch (IllegalAccessException e) {
            throw new RuntimeException("Can't notify before navigation listener change method " + method.getName(), e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException("Can't notify before navigation listener method " + method.getName(), e);
        } finally {
            if (recoverAccessibility) {
                method.setAccessible(false);
            }
        }
    }
}
